/*
Script: mootree.js
	My Object Oriented Tree

Author:
	Rasmus Schultz, <http://www.mindplay.dk>

License:
	MIT-style license.

Credits:
	Inspired by:
	- WebFX xTree, <http://webfx.eae.net/dhtml/xtree/>
	- Destroydrop dTree, <http://www.destroydrop.com/javascripts/tree/>
*/

Element.extend({
	empty: function() {
		while (this.lastChild) this.removeChild(this.lastChild);
	},
	show: function(show) {
		this.style.display = show ? 'block' : 'none';
	}
});

var MooTreeIcon = ['I','L','Lminus','Lplus','Rminus','Rplus','T','Tminus','Tplus','_closed','_doc','_open','minus','plus'];

/*
Class: MooTreeControl
	This class implements a tree control.

Properties:
	selected - returns the currently selected MooTreeNode object, or null if nothing is currently selected.

Events:
	onExpand - called when a node is expanded or collapsed: function(node, state) - where node is the MooTreeNode object that fired the event, and state is a boolean meaning true:expanded or false:collapsed.
	onSelect - called when a node is selected or deselected: function(node, state) - where node is the MooTreeNode object that fired the event, and state is a boolean meaning true:selected or false:deselected.

Parameters:
	The constructor takes two object parameters: config and options.
	The first, config, contains global settings for the tree control - you can use the configuration options listed below.
	The second, options, should contain options for the MooTreeNode constructor - please refer to the options listed in the MooTreeNode documentation.

Config:
	div - a string representing the div Element inside which to build the tree control.
	mode - optional string, defaults to 'files' - specifies default icon behavior. In 'files' mode, empty nodes have a document icon - whereas, in 'folders' mode, all nodes are displayed as folders (a'la explorer).
	grid - boolean, defaults to false. If set to true, a grid is drawn to outline the structure of the tree.
	theme - string, optional, defaults to 'mootree.gif' - specifies the 'theme' GIF to use.
	
	onExpand - optional function (see Events above)
	onSelect - optional function (see Events above)

*/

var MooTreeControl = new Class({
	
	initialize: function(config, options) {
		
		options.control = this;               // make sure our new MooTreeNode knows who it's owner control is
		options.div = config.div;             // tells the root node which div to insert itself into
		this.root = new MooTreeNode(options); // create the root node of this tree control

		this.theme = config.theme || 'mootree.gif';
		
		this.selected = null;                 // set the currently selected node to nothing
		this.mode = config.mode;              // mode can be "folders" or "files", and affects the default icons
		this.grid = config.grid;              // grid can be turned on (true) or off (false)
		
		this.onExpand = config.onExpand || new Function(); // called when any node in the tree is expanded/collapsed
		this.onSelect = config.onSelect || new Function(); // called when any node in the tree is selected/deselected
		
	},
	
	/*
	Property: insert
		Creates a new node under the root node of this tree.
	
	Parameters:
		options - an object containing the same options available to the MooTreeNode constructor.
		
	Returns:
		A new MooTreeNode instance.
	*/
	
	insert: function(options) {
		options.control = this;
		return this.root.insert(options);
	},
	
	/*
	Property: update
		Visually updates the control on screen.
		You need to call this after inserting/removing nodes.
	*/
	
	update: function() {
		this.root.update(true);
	},
	
	/*
	Property: select
		Sets the currently selected node.
		This is called by MooTreeNode when a node is selected.
	
	Parameters:
		node - the MooTreeNode object to select.
	*/
	
	select: function(node) {
		if (this.selected === node) return; // already selected
		if (this.selected) {
			// deselect previously selected node:
			this.selected.select(false);
			this.onSelect(this.selected, false);
		}
		// select new node:
		this.selected = node;
		node.select(true);
		this.onSelect(node, true);
	},
	
	/*
	Property: expand
		Expands the entire tree, recursively.
	*/
	
	expand: function() {
		this.root.toggle(true, true);
	},

	/*
	Property: collapse
		Collapses the entire tree, recursively.
	*/

	collapse: function() {
		this.root.toggle(true, false);
	},
	
	/*
	Property: adopt
		Adopts a structure of nested ul/li/a elements as tree nodes, then removes the original elements.
	
	Parameters:
		id - a string representing the ul element to be adopted, or an element reference.
		parentNode - optional, a MooTreeNode object under which to import the specified ul element.
	
	Note:
		The ul/li structure must be properly nested, and each li-element must contain one a-element, e.g.:
		><ul id="mytree">
		>  <li><a href="test.html">Item One</a></li>
		>  <li><a href="test.html">Item Two</a>
		>    <ul>
		>      <li><a href="test.html">Item Two Point One</a></li>
		>      <li><a href="test.html">Item Two Point Two</a></li>
		>    </ul>
		>  </li>
		>  <li><a href="test.html">Item Three</a></li>
		></ul>
	*/
	
	adopt: function(id, parentNode) {
		if (parentNode === undefined) parentNode = this.root;
		this._adopt(id, parentNode);
		parentNode.update(true);
		$(id).remove();
	},
	
	/*
	Property: _adopt
		Private method used by adopt - do not manually invoke this method.
	*/
	
	_adopt: function(id, parentNode) {
		/* adopts a structure of ul/li elements into this tree */
		e = $(id);
		var i=0, c = e.getChildren();
		for (i=0; i<c.length; i++) {
			if (c[i].nodeName == 'LI') {
				var href = '', label = '', node = null, subul = null;
				var n=0, s = c[i].getChildren();
				for (n=0; n<s.length; n++) {
					switch (s[n].nodeName) {
						case 'A':
							label = s[n].childNodes[0].nodeValue;
							href = s[n].getAttribute('href');
						break;
						case 'UL':
							subul = s[n];
						break;
					}
				}
				if (label) {
					node = parentNode.insert({text:label, data:{url:href}});
					if (subul) this._adopt(subul, node);
				}
			}
		}
	}
	
});

/*
Class: MooTreeNode
	This class implements the functionality of a single node in a MooTreeControl.

Note:
	You should not manually create objects of this class - rather, you should use
	the insert method of the MooTreeControl to create nodes in the root of the
	tree, and the insert method of MooTreeNode items to create subnodes. The
	insert methods have a similar syntax, and both return the newly created
	MooTreeNode object.

Parameters:
	options - an object. See options below.

Options:
	text - this is the displayed text of the node, and as such as is the only required parameter.
	color - string, optional - if specified, must be a six-digit hexadecimal RGB color code.
	
	open - boolean value, defaults to false. Use true if you want the node open from the start.
	
	icon - use this to customize the icon of the node. The following predefined values may be used: '_open', '_closed' and '_doc'. Alternatively, specify the URL of a GIF or PNG image to use - this should be exactly 18x18 pixels in size. If you have a strip of images, you can specify an image number (e.g. 'my_icons.gif#4' for icon number 4).
	openicon - use this to customize the icon of the node when it's open.
	
	data - an object containing whatever data you wish to associate with this node (such as an url and/or an id, etc.)

Events:
	onExpand - called when the node is expanded or collapsed: function(state) - where state is a boolean meaning true:expanded or false:collapsed.
	onSelect - called when the node is selected or deselected: function(state) - where state is a boolean meaning true:selected or false:deselected.
*/

var MooTreeNode = new Class({
	
	initialize: function(options) {
		
		this.text = options.text;       // the text displayed by this node
		this.nodes = new Array();       // subnodes nested beneath this node (MooTreeNode objects)
		this.parent = null;             // this node's parent node (another MooTreeNode object)
		this.last = true;               // a flag telling whether this node is the last (bottom) node of it's parent
		this.control = options.control; // owner control of this node's tree
		this.selected = false;          // a flag telling whether this node is the currently selected node in it's tree
		
		this.color = options.color || null; // text color of this node
		
		this.data = options.data; // optional object containing whatever data you wish to associate with the node (typically an url or an id)
		
		this.onExpand = options.onExpand || new Function(); // called when the individual node is expanded/collapsed
		this.onSelect = options.onSelect || new Function(); // called when the individual node is selected/deselected
		
		this.open = options.open ? true : false; // flag: node open or closed?
		
		this.icon = options.icon;
		this.openicon = options.openicon || this.icon;
		
		// create the necessary divs:
		this.div = {
			main: new Element('div').addClass('mooTree_node'),
			indent: new Element('div'),
			gadget: new Element('div'),
			icon: new Element('div'),
			text: new Element('div').addClass('mooTree_text'),
			sub: new Element('div')
		}
		
		// put the other divs under the main div:
		this.div.main.adopt(this.div.indent);
		this.div.main.adopt(this.div.gadget);
		this.div.main.adopt(this.div.icon);
		this.div.main.adopt(this.div.text);

		// put the main and sub divs in the specified parent div:
		$(options.div).adopt(this.div.main);
		$(options.div).adopt(this.div.sub);
		
		// attach event handler to gadget:
		this.div.gadget._node = this;
		this.div.gadget.onclick = this.div.gadget.ondblclick = function() {
			this._node.toggle();
		}
		
		// attach event handler to icon/text:
		this.div.icon._node = this.div.text._node = this;
		this.div.icon.onclick = this.div.icon.ondblclick = this.div.text.onclick = this.div.text.ondblclick = function() {
			this._node.control.select(this._node);
		}
		
	},
	
	/*
	Property: insert
		Creates a new node, nested inside this one.
	
	Parameters:
		options - an object containing the same options available to the MooTreeNode constructor.

	Returns:
		A new MooTreeNode instance.
	*/
	
	insert: function(options) {
		
		// set the parent div and create the node:
		options.div = this.div.sub;
		var node = new MooTreeNode(options);
		
		// set the new node's parent and control:
		node.parent = this;
		node.control = node.parent.control;
		
		// mark this node's last node as no longer being the last, then add the new last node:
		var n = this.nodes;
		if (n.length) n[n.length-1].last = false;
		n.push(node);
		
		// repaint the new node:
		node.update();
		
		// repaint the new node's parent (this node):
		if (n.length == 1) this.update();
		
		// recursively repaint the new node's previous sibling node:
		if (n.length > 1) n[n.length-2].update(true);
		
		return node;
		
	},
	
	/*
	Property: remove
		Removes this node, and all of it's child nodes.
	*/
	
	remove: function() {
		var p = this.parent;
		this._remove();
		p.update(true);
	},
	
	/*
	Property: _remove
		Private method used by remove - do not invoke this method.
	*/
	
	_remove: function() {
		
		// recursively remove this node's subnodes:
		this.nodes.forEach(function(node) {
			node._remove();
		});

		// remove this node's divs:
		this.div.main.remove();
		this.div.sub.remove();
		
		if (this.parent) {
			
			// remove this node from the parent's collection of nodes:
			var p = this.parent.nodes;
			p.remove(this);
			
			// in case we removed the parent's last node, flag it's current last node as being the last:
			if (p.length) p[p.length-1].last = true;
			
		}
		
	},
	
	/*
	Property: update
		Update the tree node's visual appearance.
	
	Parameters:
		recursive - boolean, defaults to false. If true, recursively updates all nodes beneath this one.
	*/
	
	update: function(recursive) {
		
		var x;
		
		// make selected, or not:
		this.div.main.className = 'mooTree_node' + (this.selected ? ' mooTree_selected' : '');
		
		// update indentations:
		x = this.div.indent;
		x.empty();
		var p = this, i;
		while (p.parent) {
			p = p.parent;
			i = this.getImg(p.last || !this.control.grid ? '' : 'I');
			if (x.firstChild) {
				i.injectBefore( x.firstChild );
			} else {
				x.adopt(i);
			}
		}
		
		// update the text:
		x = this.div.text;
		x.empty();
		x.appendText(this.text);
		if (this.color) x.style.color = '#'+this.color;
		
		// update the icon:
		x = this.div.icon;
		x.empty();
		this.getImg( this.nodes.length ? ( this.open ? (this.openicon || this.icon || '_open') : (this.icon || '_closed') ) : ( this.icon || (this.control.mode == 'folders' ? '_closed' : '_doc') ), x );
		
		// update the plus/minus gadget:
		x = this.div.gadget;
		x.empty();
		this.getImg( ( this.control.grid ? ( this.control.root == this ? 'R' : (this.last?'L':'T') ) : '') + (this.nodes.length ? (this.open?'minus':'plus') : ''), x );
		
		// show/hide subnodes:
		this.div.sub.show(this.open);
		
		// if recursively updating, update all child nodes:
		if (recursive) this.nodes.forEach( function(node) {
			node.update(true);
		});
		
	},
	
	/*
	Property: getImg
		Creates a new image (actually, a div Element) - or turns a given div into an image.
		You should not need to manually call this method.
	
	Parameters:
		name - the name of new image to create, defined by MooTreeIcon or located in an external file.
		div - optional. A string representing an existing div element to be turned into an image, or an element reference.
	
	Returns:
		The new div Element.
	*/
	
	getImg: function(name, div) {
		
		// if no div was specified, create a new one:
		if (div === undefined) div = new Element('div');
		
		// apply the mooTree_img CSS class:
		div.addClass('mooTree_img');
		
		// if a blank image was requested, simply return it now:
		if (name == '') return div;
		
		var img = this.control.theme;
		var i = MooTreeIcon.indexOf(name);
		if (i == -1) {
			// custom (external) icon:
			var x = name.split('#');
			img = x[0];
			i = (x.length == 2 ? parseInt(x[1])-1 : 0);
		}
		div.style.backgroundImage = 'url(' + img + ')';
		div.style.backgroundPosition = '-' + (i*18) + 'px 0px';
		
		return div;
		
	},
	
	/*
	Property: toggle
		By default (with no arguments) this function toggles the node between expanded/collapsed.
		Can also be used to recursively expand/collapse all or part of the tree.
	
	Parameters:
		recursive - boolean, defaults to false. With recursive set to true, all child nodes are recursively toggle to this node's new state.
		state - boolean. If undefined, the node's state is toggled. If true or false, the node can be explicitly opened or closed.
	*/
	
	toggle: function(recursive, state) {
		
		this.open = (state === undefined ? !this.open : state);
		this.update();

		this.onExpand(this.open);
		this.control.onExpand(this, this.open);

		if (recursive) this.nodes.forEach( function(node) {
			node.toggle(true, this.open);
		}, this);
		
	},
	
	/*
	Property: select
		Called by MooTreeControl when the selection changes.
		You should not manually call this method - to set the selection, use the MooTreeControl.select method.
	*/
	
	select: function(state) {
		
		this.selected = state;
		this.update();
		this.onSelect(state);
		
	}
	
});
